Micron Document
baubs git

Node / mirrors / respira / commits / ba38072

Commit ba380723c0ebc6e481cea82f78821bcbd0e48bfb


Parents : 11b710e
Author : Jan-Henrik Bruhn <hi@jhbruhn.de>
Date : 2025-12-14T12:27:24+01:00

feature: Refactor PEN parser to decoder with coherent types

Renamed and restructured PEN parsing to match encoder pattern:

Changes:
- Renamed pen/parser.ts → pen/decoder.ts (consistent with encoder.ts)
- Created pen/types.ts for PEN-specific type definitions
- Moved types out of machine.ts (they're format-specific, not machine-specific)
- Unified decoder with encoder test helpers (encoder.test.ts now uses decoder)

New structure:
- decodePenStitch() - decode single 4-byte stitch
- decodeAllPenStitches() - decode all stitches from bytes
- decodePenData() - full decode with color blocks and bounds
- getStitchColor() - helper to get color for a stitch index

Type definitions:
- DecodedPenStitch - individual stitch with coordinates and flags
- PenColorBlock - color block (stitch range for one thread)
- DecodedPenData - complete decoded pattern data

Backward compatibility:
- Added compatibility aliases (isJump, flags, startStitch, endStitch)
- Maintains API compatibility with existing UI code

Also added dist-electron to eslint ignore list.

All tests passing (27/27), build successful, lint clean.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Changes

6 files changed, 239 insertions(+), 206 deletions(-)


Diff

diff --git a/eslint.config.mjs b/eslint.config.mjs
index ff46326..a9c0bff 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -6,7 +6,7 @@ import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
- globalIgnores(['dist', '.vite']),
+ globalIgnores(['dist', 'dist-electron', '.vite']),
{
files: ['**/*.{ts,tsx}'],
extends: [

diff --git a/src/formats/import/client.ts b/src/formats/import/client.ts
index 785f394..ef4bd53 100644
--- a/src/formats/import/client.ts
+++ b/src/formats/import/client.ts
@@ -1,7 +1,7 @@
import type { WorkerMessage, WorkerResponse } from './worker';
import PatternConverterWorker from './worker?worker';
-import { parsePenData } from '../pen/parser';
-import type { PenData } from '../../types/machine';
+import { decodePenData } from '../pen/decoder';
+import type { DecodedPenData } from '../pen/types';
export type PyodideState = 'not_loaded' | 'loading' | 'ready' | 'error';
@@ -24,8 +24,8 @@ export interface PesPatternData {
chart: string | null;
threadIndices: number[];
}>;
- penData: Uint8Array; // Raw PEN bytes sent to machine
- penStitches: PenData; // Parsed PEN stitches (for rendering)
+ penData: Uint8Array; // Raw PEN bytes sent to machine
+ penStitches: DecodedPenData; // Decoded PEN stitches (for rendering)
colorCount: number;
stitchCount: number;
bounds: {
@@ -180,9 +180,9 @@ class PatternConverterClient {
// Convert penData array back to Uint8Array
const penData = new Uint8Array(message.data.penData);
- // Parse the PEN data to get stitches for rendering
- const penStitches = parsePenData(penData);
- console.log('[PatternConverter] Parsed PEN data:', penStitches.totalStitches, 'stitches,', penStitches.colorCount, 'colors');
+ // Decode the PEN data to get stitches for rendering
+ const penStitches = decodePenData(penData);
+ console.log('[PatternConverter] Decoded PEN data:', penStitches.stitches.length, 'stitches,', penStitches.colorBlocks.length, 'colors');
const result: PesPatternData = {
...message.data,

diff --git a/src/formats/pen/decoder.ts b/src/formats/pen/decoder.ts
new file mode 100644
index 0000000..50e910f
--- /dev/null
+++ b/src/formats/pen/decoder.ts
@@ -0,0 +1,179 @@
+/**
+ * PEN Format Decoder
+ *
+ * This module contains the logic for decoding Brother PP1 PEN format embroidery files.
+ * The PEN format uses absolute coordinates shifted left by 3 bits, with flags in the low 3 bits.
+ */
+
+import type { DecodedPenStitch, DecodedPenData, PenColorBlock } from './types';
+
+// PEN format flags
+const PEN_FEED_DATA = 0x01; // Bit 0: Jump stitch (move without stitching)
+const PEN_CUT_DATA = 0x02; // Bit 1: Trim/cut thread command
+const PEN_COLOR_END = 0x03; // Last stitch before color change
+const PEN_DATA_END = 0x05; // Last stitch of entire pattern
+
+/**
+ * Decode a single PEN stitch (4 bytes) into coordinates and flags
+ *
+ * @param bytes The byte array containing PEN data
+ * @param offset The offset in bytes to start reading from
+ * @returns Decoded stitch with coordinates and flag information
+ */
+export function decodePenStitch(
+ bytes: Uint8Array | number[],
+ offset: number
+): DecodedPenStitch {
+ const xLow = bytes[offset];
+ const xHigh = bytes[offset + 1];
+ const yLow = bytes[offset + 2];
+ const yHigh = bytes[offset + 3];
+
+ const xRaw = xLow | (xHigh << 8);
+ const yRaw = yLow | (yHigh << 8);
+
+ // Extract flags from low 3 bits
+ const xFlags = xRaw & 0x07;
+ const yFlags = yRaw & 0x07;
+
+ // Clear flags and shift right to get actual coordinates
+ const xClean = xRaw & 0xFFF8;
+ const yClean = yRaw & 0xFFF8;
+
+ // Convert to signed 16-bit
+ let xSigned = xClean;
+ let ySigned = yClean;
+ if (xSigned > 0x7FFF) xSigned = xSigned - 0x10000;
+ if (ySigned > 0x7FFF) ySigned = ySigned - 0x10000;
+
+ // Shift right by 3 to get actual coordinates
+ const x = xSigned >> 3;
+ const y = ySigned >> 3;
+
+ const isFeed = (yFlags & PEN_FEED_DATA) !== 0;
+ const isCut = (yFlags & PEN_CUT_DATA) !== 0;
+ const isColorEnd = xFlags === PEN_COLOR_END;
+ const isDataEnd = xFlags === PEN_DATA_END;
+
+ return {
+ x,
+ y,
+ xFlags,
+ yFlags,
+ isFeed,
+ isCut,
+ isColorEnd,
+ isDataEnd,
+ // Compatibility aliases
+ isJump: isFeed,
+ flags: (xFlags & 0x07) | (yFlags & 0x07),
+ };
+}
+
+/**
+ * Decode all stitches from PEN format bytes
+ *
+ * @param bytes PEN format byte array
+ * @returns Array of decoded stitches
+ */
+export function decodeAllPenStitches(bytes: Uint8Array | number[]): DecodedPenStitch[] {
+ if (bytes.length < 4 || bytes.length % 4 !== 0) {
+ throw new Error(`Invalid PEN data size: ${bytes.length} bytes (must be multiple of 4)`);
+ }
+
+ const stitches: DecodedPenStitch[] = [];
+ for (let i = 0; i < bytes.length; i += 4) {
+ stitches.push(decodePenStitch(bytes, i));
+ }
+ return stitches;
+}
+
+/**
+ * Decode PEN format data into a complete pattern structure
+ *
+ * This function parses PEN bytes and extracts:
+ * - Individual stitches with coordinates and flags
+ * - Color blocks (groups of stitches using the same thread color)
+ * - Pattern bounds
+ *
+ * @param data PEN format byte array
+ * @returns Complete decoded pattern data
+ */
+export function decodePenData(data: Uint8Array): DecodedPenData {
+ const stitches = decodeAllPenStitches(data);
+ const colorBlocks: PenColorBlock[] = [];
+
+ let minX = Infinity;
+ let maxX = -Infinity;
+ let minY = Infinity;
+ let maxY = -Infinity;
+
+ let currentColorStart = 0;
+ let currentColor = 0;
+
+ for (let i = 0; i < stitches.length; i++) {
+ const stitch = stitches[i];
+
+ // Track bounds (exclude jump stitches)
+ if (!stitch.isFeed) {
+ minX = Math.min(minX, stitch.x);
+ maxX = Math.max(maxX, stitch.x);
+ minY = Math.min(minY, stitch.y);
+ maxY = Math.max(maxY, stitch.y);
+ }
+
+ // Check for color change or data end
+ if (stitch.isColorEnd) {
+ colorBlocks.push({
+ startStitchIndex: currentColorStart,
+ endStitchIndex: i,
+ colorIndex: currentColor,
+ // Compatibility aliases
+ startStitch: currentColorStart,
+ endStitch: i,
+ });
+ currentColor++;
+ currentColorStart = i + 1;
+ } else if (stitch.isDataEnd) {
+ // Final color block
+ if (currentColorStart <= i) {
+ colorBlocks.push({
+ startStitchIndex: currentColorStart,
+ endStitchIndex: i,
+ colorIndex: currentColor,
+ // Compatibility aliases
+ startStitch: currentColorStart,
+ endStitch: i,
+ });
+ }
+ break;
+ }
+ }
+
+ return {
+ stitches,
+ colorBlocks,
+ bounds: {
+ minX: minX === Infinity ? 0 : minX,
+ maxX: maxX === -Infinity ? 0 : maxX,
+ minY: minY === Infinity ? 0 : minY,
+ maxY: maxY === -Infinity ? 0 : maxY,
+ },
+ };
+}
+
+/**
+ * Get the color index for a stitch at the given index
+ *
+ * @param penData Decoded PEN pattern data
+ * @param stitchIndex Index of the stitch
+ * @returns Color index, or -1 if not found
+ */
+export function getStitchColor(penData: DecodedPenData, stitchIndex: number): number {
+ for (const block of penData.colorBlocks) {
+ if (stitchIndex >= block.startStitchIndex && stitchIndex <= block.endStitchIndex) {
+ return block.colorIndex;
+ }
+ }
+ return -1;
+}

diff --git a/src/formats/pen/encoder.test.ts b/src/formats/pen/encoder.test.ts
index e77f930..5ecb553 100644
--- a/src/formats/pen/encoder.test.ts
+++ b/src/formats/pen/encoder.test.ts
@@ -5,74 +5,12 @@ import {
generateLockStitches,
encodeStitchesToPen,
} from './encoder';
+import { decodeAllPenStitches } from './decoder';
import { STITCH, MOVE, TRIM, END } from '../import/constants';
// PEN format flag constants for testing
const PEN_FEED_DATA = 0x01;
const PEN_CUT_DATA = 0x02;
-const PEN_COLOR_END = 0x03;
-const PEN_DATA_END = 0x05;
-
-/**
- * Helper function to decode a single PEN stitch (4 bytes) into coordinates and flags
- */
-function decodePenStitch(bytes: number[], offset: number): {
- x: number;
- y: number;
- xFlags: number;
- yFlags: number;
- isFeed: boolean;
- isCut: boolean;
- isColorEnd: boolean;
- isDataEnd: boolean;
-} {
- const xLow = bytes[offset];
- const xHigh = bytes[offset + 1];
- const yLow = bytes[offset + 2];
- const yHigh = bytes[offset + 3];
-
- const xRaw = xLow | (xHigh << 8);
- const yRaw = yLow | (yHigh << 8);
-
- // Extract flags from low 3 bits
- const xFlags = xRaw & 0x07;
- const yFlags = yRaw & 0x07;
-
- // Clear flags and shift right to get actual coordinates
- const xClean = xRaw & 0xFFF8;
- const yClean = yRaw & 0xFFF8;
-
- // Convert to signed
- let xSigned = xClean;
- let ySigned = yClean;
- if (xSigned > 0x7FFF) xSigned = xSigned - 0x10000;
- if (ySigned > 0x7FFF) ySigned = ySigned - 0x10000;
-
- const x = xSigned >> 3;
- const y = ySigned >> 3;
-
- return {
- x,
- y,
- xFlags,
- yFlags,
- isFeed: (yFlags & PEN_FEED_DATA) !== 0,
- isCut: (yFlags & PEN_CUT_DATA) !== 0,
- isColorEnd: xFlags === PEN_COLOR_END,
- isDataEnd: xFlags === PEN_DATA_END,
- };
-}
-
-/**
- * Helper to parse all stitches from PEN bytes
- */
-function decodeAllPenStitches(bytes: number[]) {
- const stitches = [];
- for (let i = 0; i < bytes.length; i += 4) {
- stitches.push(decodePenStitch(bytes, i));
- }
- return stitches;
-}
describe('encodeStitchPosition', () => {
it('should encode position (0, 0) correctly', () => {

diff --git a/src/formats/pen/parser.ts b/src/formats/pen/parser.ts
deleted file mode 100644
index 80e68e7..0000000
--- a/src/formats/pen/parser.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-import type { PenData, PenStitch, PenColorBlock } from '../../types/machine';
-
-// PEN format flags
-const PEN_FEED_DATA = 0x01; // Y-coordinate low byte, bit 0
-const PEN_COLOR_END = 0x03; // X-coordinate low byte, bits 0-2
-const PEN_DATA_END = 0x05; // X-coordinate low byte, bits 0-2
-
-export function parsePenData(data: Uint8Array): PenData {
- if (data.length < 4 || data.length % 4 !== 0) {
- throw new Error(`Invalid PEN data size: ${data.length} bytes`);
- }
-
- const stitches: PenStitch[] = [];
- const colorBlocks: PenColorBlock[] = [];
- const stitchCount = data.length / 4;
-
- let currentColorStart = 0;
- let currentColor = 0;
- let minX = Infinity, maxX = -Infinity;
- let minY = Infinity, maxY = -Infinity;
-
- console.log(`Parsing PEN data: ${data.length} bytes, ${stitchCount} stitches`);
-
- for (let i = 0; i < stitchCount; i++) {
- const offset = i * 4;
-
- // Extract coordinates (shifted left by 3 bits in PEN format)
- const xRaw = data[offset] | (data[offset + 1] << 8);
- const yRaw = data[offset + 2] | (data[offset + 3] << 8);
-
- // Extract flags from low 3 bits
- const xFlags = data[offset] & 0x07;
- const yFlags = data[offset + 2] & 0x07;
-
- // Decode coordinates (shift right by 3 to get actual position)
- // The coordinates are stored as signed 16-bit values, left-shifted by 3
- // Step 1: Clear the flag bits (low 3 bits) from the raw values
- const xRawClean = xRaw & 0xFFF8;
- const yRawClean = yRaw & 0xFFF8;
-
- // Step 2: Convert from unsigned 16-bit to signed 16-bit
- let xSigned = xRawClean;
- let ySigned = yRawClean;
- if (xSigned > 0x7FFF) xSigned = xSigned - 0x10000;
- if (ySigned > 0x7FFF) ySigned = ySigned - 0x10000;
-
- // Step 3: Shift right by 3 (arithmetic shift, preserves sign)
- const x = xSigned >> 3;
- const y = ySigned >> 3;
-
- const stitch: PenStitch = {
- x,
- y,
- flags: (xFlags & 0x07) | (yFlags & 0x07),
- isJump: (yFlags & PEN_FEED_DATA) !== 0,
- };
-
- stitches.push(stitch);
-
- // Track bounds
- if (!stitch.isJump) {
- minX = Math.min(minX, x);
- maxX = Math.max(maxX, x);
- minY = Math.min(minY, y);
- maxY = Math.max(maxY, y);
- }
-
- // Check for color change or data end
- if (xFlags === PEN_COLOR_END) {
- const block: PenColorBlock = {
- startStitch: currentColorStart,
- endStitch: i,
- colorIndex: currentColor,
- };
- colorBlocks.push(block);
-
- console.log(
- `Color ${currentColor}: stitches ${currentColorStart}-${i} (${
- i - currentColorStart + 1
- } stitches)`
- );
-
- currentColor++;
- currentColorStart = i + 1;
- } else if (xFlags === PEN_DATA_END) {
- if (currentColorStart < i) {
- const block: PenColorBlock = {
- startStitch: currentColorStart,
- endStitch: i,
- colorIndex: currentColor,
- };
- colorBlocks.push(block);
-
- console.log(
- `Color ${currentColor} (final): stitches ${currentColorStart}-${i} (${
- i - currentColorStart + 1
- } stitches)`
- );
-
- currentColor++;
- }
- console.log(`Data end marker at stitch ${i}`);
- break;
- }
- }
-
- const result: PenData = {
- stitches,
- colorBlocks,
- totalStitches: stitches.length,
- colorCount: colorBlocks.length,
- bounds: {
- minX: minX === Infinity ? 0 : minX,
- maxX: maxX === -Infinity ? 0 : maxX,
- minY: minY === Infinity ? 0 : minY,
- maxY: maxY === -Infinity ? 0 : maxY,
- },
- };
-
- console.log(
- `Parsed: ${result.totalStitches} stitches, ${result.colorCount} colors`
- );
- console.log(`Bounds: (${result.bounds.minX}, ${result.bounds.minY}) to (${result.bounds.maxX}, ${result.bounds.maxY})`);
-
- return result;
-}
-
-export function getStitchColor(penData: PenData, stitchIndex: number): number {
- for (const block of penData.colorBlocks) {
- if (stitchIndex >= block.startStitch && stitchIndex <= block.endStitch) {
- return block.colorIndex;
- }
- }
- return -1;
-}

diff --git a/src/formats/pen/types.ts b/src/formats/pen/types.ts
new file mode 100644
index 0000000..e27f7b9
--- /dev/null
+++ b/src/formats/pen/types.ts
@@ -0,0 +1,51 @@
+/**
+ * PEN Format Types
+ *
+ * Type definitions for decoded PEN format data.
+ * These types represent the parsed structure of Brother PP1 PEN embroidery files.
+ */
+
+/**
+ * A single decoded PEN stitch with coordinates and flags
+ */
+export interface DecodedPenStitch {
+ x: number; // X coordinate (already shifted right by 3)
+ y: number; // Y coordinate (already shifted right by 3)
+ xFlags: number; // Flags from X coordinate low 3 bits
+ yFlags: number; // Flags from Y coordinate low 3 bits
+ isFeed: boolean; // Jump/move without stitching (Y-bit 0)
+ isCut: boolean; // Trim/cut thread (Y-bit 1)
+ isColorEnd: boolean; // Color change marker (X-bits 0-2 = 0x03)
+ isDataEnd: boolean; // Pattern end marker (X-bits 0-2 = 0x05)
+
+ // Compatibility aliases
+ isJump: boolean; // Alias for isFeed (backward compatibility)
+ flags: number; // Combined flags (backward compatibility)
+}
+
+/**
+ * A color block representing stitches of the same thread color
+ */
+export interface PenColorBlock {
+ startStitchIndex: number; // Index of first stitch in this color
+ endStitchIndex: number; // Index of last stitch in this color
+ colorIndex: number; // Color number (0-based)
+
+ // Compatibility aliases
+ startStitch: number; // Alias for startStitchIndex (backward compatibility)
+ endStitch: number; // Alias for endStitchIndex (backward compatibility)
+}
+
+/**
+ * Complete decoded PEN pattern data
+ */
+export interface DecodedPenData {
+ stitches: DecodedPenStitch[];
+ colorBlocks: PenColorBlock[];
+ bounds: {
+ minX: number;
+ maxX: number;
+ minY: number;
+ maxY: number;
+ };
+}

Served by rngit 1.4.2 - Generated in 0.06s